package com.lzh.hosp.websocket;

import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lzh.hosp.model.Message;
import com.lzh.hosp.model.Prescription;
import com.lzh.hosp.model.ServerNode;
import com.lzh.hosp.service.PrescriptionService;
import com.lzh.hosp.utils.RedisCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
@ServerEndpoint("/ws/{role}/{userId}")
public class WebSocketServer {
    private static final Logger LOG = LoggerFactory.getLogger(WebSocketServer.class);

    @Autowired
    private RedisCache redisCache;

    @Autowired
    private PrescriptionService prescriptionService;

    /**
     * 每个客户端一个userId
     */
    private Long userId;
    private String role = "";
    private ServerNode serverNode;

    private static Map<Long, Session> ticketMap = new ConcurrentHashMap<>();
    private static Map<Long, Session> notifyMap = new ConcurrentHashMap<>();
    private static Map<Long, Session> drugstoreNotifyMap = new ConcurrentHashMap<>();
    private static Map<Long, Session> docMap = new ConcurrentHashMap<>();
    private static List<ServerNode> drugstore = new ArrayList<>();

    public WebSocketServer(){
        //将药房客户端列表设为线程安全
        this.drugstore = Collections.synchronizedList(this.drugstore);
    }


    /**
     * 连接成功
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") Long userId ,@PathParam("role") String role) {
        switch (role){
            case "ticket":
                ticketMap.put(userId,session);
                break;
            case "notify":
                notifyMap.put(userId,session);
                break;
            case "doctor":
                docMap.put(userId,session);
                break;
            case "drugstore":
                serverNode = new ServerNode(session,2);
                serverNode.setId(userId);
                drugstore.add(serverNode);
                break;
            case "drugstoreNotify":
                drugstoreNotifyMap.put(userId,session);
                break;
            default:
                break;
        }
        this.userId = userId;
        this.role = role;
        LOG.info("有新连接：userId：{}，session id：{}，当前取号连接数：{}，当前叫号连接数：{}，当前医生连接数：{}，当前药房连接数：{}"
                , userId, session.getId(), ticketMap.size(), notifyMap.size(), docMap.size(),drugstore.size());
    }

    /**
     * 连接关闭
     */
    @OnClose
    public void onClose(Session session) {
        System.out.println("关闭");
        switch (this.role){
            case "ticket":
                ticketMap.remove(this.userId);
                break;
            case "notify":
                notifyMap.remove(this.userId);
                break;
            case "doctor":
                docMap.remove(this.userId);
                break;
            case "drugstore":
                drugstore.remove(serverNode);
            default:
                break;
        }
        LOG.info("连接关闭：userId：{}，session id：{}，当前取号连接数：{}，当前叫号连接数：{}，当前医生连接数：{}，当前药房连接数：{}"
                , userId, session.getId(), ticketMap.size(), notifyMap.size(), docMap.size(),drugstore.size());    }

    /**
     * 收到消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        try {
            Message msg = new ObjectMapper().readValue(message, Message.class);
            LOG.info("收到消息：{}，内容：{}", userId, msg);
            //根据msg的参数进行不同的操作
            switch (msg.getAction()){
                case "updatePatList":{
                    //遍历docMap找到对应的医生客户端
                    for (Long key : docMap.keySet()) {
                        if (key==msg.getReceiverCode()){
                            Session docSession = docMap.get(key);
                            docSession.getBasicRemote().sendText(JSONObject.toJSONString(msg));
                        }
                    }
                    for (Long key : notifyMap.keySet()) {
                        if (key==msg.getReceiverDepcode()){
                            Session notifySession = notifyMap.get(key);
                            notifySession.getBasicRemote().sendText(JSONObject.toJSONString(msg));
                        }
                    }
                    break;
                }
                case "notifyPatient":
                case "updatePatientToNotify": {
                    for (Long key : notifyMap.keySet()) {
                        if (key==msg.getReceiverDepcode()){
                            Session notifySession = notifyMap.get(key);
                            notifySession.getBasicRemote().sendText(JSONObject.toJSONString(msg));
                        }
                    }
                    break;
                }
                case "notifyDrugstoreMedicineCollection":{
                    //取出message中的信息
                    String prescriptionId = (String) msg.getParams().get("prescriptionId");

                    //使用加权轮询算法选出药房
                    ServerNode serverNode = selectNode();
                    if (StringUtils.isEmpty(serverNode)){
                        throw new RuntimeException("药房端尚未连接");
                    }
                    System.out.println(serverNode);

                    //保存取药患者信息，引导患者去对应的药房取药
                    saveDrugstorePatient(serverNode.getId(),prescriptionId);

                    //向药房端发送信息
                    serverNode.getSession().getBasicRemote().sendText(JSONObject.toJSONString(new Message("updatePatientList",null,null,null)));
                    //通知电子显示大屏更新信息
                    Message newMsg = new Message("notifyDrugstoreDesktopUpdatePatList",null,null,null);
                    //遍历drugstoreNotifyMap找到对应的药库客户端
                    for (Long key : drugstoreNotifyMap.keySet()) {
                        if (key==serverNode.getId()){
                            Session drugstoreNotifySession = drugstoreNotifyMap.get(key);
                            drugstoreNotifySession.getBasicRemote().sendText(JSONObject.toJSONString(newMsg));
                        }
                    }
                    break;
                }
                case "notifyDrugstoreDesktopUpdatePatList":{
                    //遍历docMap找到对应的药库客户端
                    for (Long key : drugstoreNotifyMap.keySet()) {
                        if (key==msg.getReceiverCode()){
                            Session drugstoreNotifySession = drugstoreNotifyMap.get(key);
                            drugstoreNotifySession.getBasicRemote().sendText(JSONObject.toJSONString(msg));
                        }
                    }
                    break;
                }
                default: return;
            }
            //发送给其他端口
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 连接错误
     */
    @OnError
    public void onError(Session session, Throwable error) {
        LOG.error("发生错误", error);
    }

    /**
     * 按照当前权重（currentWeight）最大值获取IP
     * @return ServerNode
     */
    public ServerNode selectNode(){
        if (drugstore.size() <= 0) return null;
        if (drugstore.size() == 1)
            return (drugstore.get(0).isAvailable()) ? drugstore.get(0) : null;

        // 权重之和
        Integer totalWeight = 0;
        ServerNode nodeOfMaxWeight = null; // 保存轮询选中的节点信息
        synchronized (drugstore){
            StringBuffer sb1 = new StringBuffer();
            StringBuffer sb2 = new StringBuffer();
            sb1.append(Thread.currentThread().getName()+"==加权轮询--[当前权重]值的变化："+printCurrentWeight(drugstore));
            // 有限权重总和可能发生变化
            for(ServerNode serverNode : drugstore){
                totalWeight += serverNode.getEffectiveWeight();
            }

            // 选出当前权重最大的节点
            ServerNode tempNodeOfMaxWeight = drugstore.get(0);
            for (ServerNode serverNode : drugstore) {
                if (serverNode.isAvailable()) {
                    serverNode.onInvokeSuccess();//提权
                    sb2.append(Thread.currentThread().getName()+"==[正常节点]："+serverNode+"\n");
                } else {
                    serverNode.onInvokeFault();//降权
                    sb2.append(Thread.currentThread().getName()+"==[宕机节点]："+serverNode+"\n");
                }

                tempNodeOfMaxWeight = tempNodeOfMaxWeight.compareTo(serverNode) > 0 ? tempNodeOfMaxWeight : serverNode;
            }
            // 必须new个新的节点实例来保存信息，否则引用指向同一个堆实例，后面的set操作将会修改节点信息
            nodeOfMaxWeight = new ServerNode(tempNodeOfMaxWeight.getSession(),tempNodeOfMaxWeight.getCurrentWeight(),tempNodeOfMaxWeight.getId());
//            nodeOfMaxWeight = new ServerNode(tempNodeOfMaxWeight.getIp(),tempNodeOfMaxWeight.getWeight(),tempNodeOfMaxWeight.isAvailable());
            nodeOfMaxWeight.setEffectiveWeight(tempNodeOfMaxWeight.getEffectiveWeight());
            nodeOfMaxWeight.setCurrentWeight(tempNodeOfMaxWeight.getCurrentWeight());

            // 调整当前权重比：按权重（effectiveWeight）的比例进行调整，确保请求分发合理。
            tempNodeOfMaxWeight.setCurrentWeight(tempNodeOfMaxWeight.getCurrentWeight() - totalWeight);
            sb1.append(" -> "+printCurrentWeight(drugstore));

            drugstore.forEach(serverNode -> serverNode.setCurrentWeight(serverNode.getCurrentWeight()+serverNode.getEffectiveWeight()));

            sb1.append(" -> "+printCurrentWeight(drugstore));
            System.out.print(sb2);  //所有节点的当前信息
            System.out.println(sb1); //打印当前权重变化过程
        }
        return nodeOfMaxWeight;
    }

    public void saveDrugstorePatient(Long userId,String prescriptionId){
        //从redis中取出需要取药的病人列表
        List<String> prescriptionList = (List<String>) redisCache.getCacheObject("drugstore:"+userId+":wait");
        if (StringUtils.isEmpty(prescriptionList)){
            prescriptionList = new ArrayList<>();
        }
        prescriptionList.add(prescriptionId);
        redisCache.setCacheObject("drugstore:"+userId+":wait",prescriptionList);

        //查询出处方,并保存药房信息
        Prescription prescription = prescriptionService.getById(prescriptionId);
        prescription.setDrugstoreId(userId);
        prescriptionService.updateById(prescription);
    }
    // 格式化打印信息
    private String printCurrentWeight(List<ServerNode> serverNodes){
        StringBuffer stringBuffer = new StringBuffer("[");
        serverNodes.forEach(node -> stringBuffer.append(node.getCurrentWeight()+",") );
        return stringBuffer.substring(0, stringBuffer.length() - 1) + "]";
    }

}
